/************************************************************************
Copyright 2003, 2004, 2005, 2006, 2007, 2008 Grid Technology Research Center,
National Institute of Advanced Industrial Science and Technology.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
************************************************************************/

package jp.aist.gtrc.plus.reserve;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.Locale;
import java.util.TreeSet;

import jp.aist.gtrc.plus.scheduler.util.CollectionUtil;
import jp.aist.gtrc.plus.scheduler.util.TextUtil;

public class ReserveLogger {

	private final File file;
	private Calendar logStartedTime = null;
	public static final String FILENAME = "reserves.log";
	private static final String CALENDAR_STRING_FORMAT = "%1$tY%1$tm%1$td%1$tH%1$tM%1$tS%1$tz";
	private static final String CALENDAR_PARSE_FORMAT = "yyyyMMddHHmmssz";
	private static final String LINE_SEPARATOR = "!";
	private static final String USERS_SEPARATOR = " ";
	private static final String NODES_SEPARATOR = " ";
	private static SimpleDateFormat calFmt = null;

	public ReserveLogger(String path) {

		String filePath = path + "/" + FILENAME;
		this.file = new File(filePath);

	}

	private String formatTime(Calendar c) {

		return String.format(Locale.US, CALENDAR_STRING_FORMAT, c);

	}

	private Calendar parseTime(String source) throws ParseException {

		if (calFmt == null) {
			calFmt = new SimpleDateFormat(CALENDAR_PARSE_FORMAT);
		}
		Calendar c = Calendar.getInstance();
		Date d = calFmt.parse(source);
		c.setTime(d);
		return c;
	}

	private void addValue(StringBuffer line, String value) {

		/*
		 * NOTE: separate values by "!", not ","
		 * because getOptionsInLine() may have ",".
		 * For example, "plus_reserve .... -sgeq A.q,B.q" makes
		 * "COMMIT_TIME!reserve!....!sgeqname=A.q,B.q".
		 */
		line.append(LINE_SEPARATOR);
		line.append(value);

	}

	private String makeMessage(ReserveOperation ope) {

		ReserveOperationType opeType = ope.getOperationType();
		ReserveInfo info = ope.getTargetReserveInfo();

		StringBuffer line = new StringBuffer(formatTime(Calendar.getInstance()));
		addValue(line, opeType.name());
		addValue(line, info.getReserveId().toString());
		addValue(line, info.getOwner());
		addValue(line, TextUtil.fromArray(info.getUsers(), USERS_SEPARATOR));
		addValue(line, formatTime(info.getStartTime()));
		addValue(line, formatTime(info.getEndTime()));
		addValue(line, TextUtil.fromArray(
				info.getReservedNodeNames(), NODES_SEPARATOR));
		addValue(line, info.getOptionsInLine());
		return line.toString();

	}

	void output(ReserveOperation ope)  {

		try {
			final boolean append = true;
			PrintWriter writer = new PrintWriter(new FileWriter(file, append));
			String msg = makeMessage(ope);
			writer.println(msg);
			writer.close();
		} catch (IOException e) {
			System.err.println("cannot output from ReserveLogger");
		}

	}

	private String getArg(String[] args, int index) {
		return (index < args.length) ? args[index] : "";
	}

	private ReserveStatus makeInstance(String line) {

		final int MIN_ARGS = 7; // {opTime, opName, rsvId, owner, users, start, end}

		String[] args = line.split(LINE_SEPARATOR);
		if (args.length < MIN_ARGS) {
			return null;
		}

		Calendar opTime = null, start = null, end = null;
		ReserveOperationType opType = null;
		try {
			opTime = parseTime(getArg(args, 0));
			if (logStartedTime == null) {
				logStartedTime = opTime;

			}

			opType = ReserveOperationType.valueOf(getArg(args, 1));
			start = parseTime(getArg(args, 5));
			end = parseTime(getArg(args, 6));
		} catch (ParseException e1) {
			// ignore invalid reservation information
			return null;
		}

		switch (opType) {
		case cancel:
		case destroy:
		case remove:
			if (opTime.before(start)) {
				// opTime < start
				// reservation was erased before reserve start
				return null;
			} else if (opTime.before(end)) {
				// start <= opTime < end
				// reservation period was finished before original end
				end = opTime;
			}
			break;
		default:
			break;
		}

		ReserveId rsvId = ReserveId.getInstance(getArg(args, 2));
		String owner = getArg(args, 3);
		String[] users = getArg(args, 4).split(USERS_SEPARATOR);
		String[] nodes = getArg(args, 7).split(NODES_SEPARATOR);
		/*
		 * NOTE: options are not referenced now
		HashMap<String, String> options = new HashMap<String, String>();
		for (String optEntry : getArg(args, 8).split(" ")) {
			String[] s = optEntry.split("=");
			if (s.length == 2) {
				options.put(s[0], s[1]);
			}
		}
		*/

		try {
			ReserveRequest req = new ReserveRequest(owner, users, nodes.length, start, end);
			ReserveStatus status = new ReserveStatus(rsvId, req, nodes);
			return status;
		} catch (ReserveException e2) {
			// ignore invalid reservation information
			return null;
		}

	}

	private boolean isMatch(ReserveStatus status,
			String owner, Calendar start, Calendar end, String[] nodes) {

		if ((owner != null) && (owner.equals(status.getOwner()) == false)) {
			return false;
		}
		if ((start != null) && status.getEndTime().before(start)) {
			// rsvEnd < start (exclude equal)
			return false;
		}
		if ((end != null) && (status.getStartTime().before(end) == false)) {
			// end <= rsvStart (include equal), equivalent to !(rsvStart < end)
			return false;
		}
		if (nodes != null) {
			for (String rsvNodeName : status.getReservedNodeNames()) {
				for (String nodeName : nodes) {
					if (nodeName.equals(rsvNodeName)) {
						return true;
					}
				}
				return false;
			}
		}
		return true;

	}

	public Collection<ReserveStatus> loadAll() throws ReserveException {

		return loadAll(null, null, null, null);

	}

	public Collection<ReserveStatus> loadAll(
			String owner, Calendar start, Calendar end, String[] nodes) throws ReserveException {

		BufferedReader reader = null;
		try {
			reader = new BufferedReader(new FileReader(file));
			TreeSet<ReserveStatus> set = new TreeSet<ReserveStatus>();

			logStartedTime = null;
			String line;
			while (true) {
				line = reader.readLine();
				if (line == null) {
					break;	// reached EOF
				}
				ReserveStatus status = makeInstance(line);
				if (status == null) {
					continue;	// something invalid line, ignore
				}

				if (CollectionUtil.isEmpty(status.getReservedNodeNames())) {
					continue;	// this reserve was expired/canceled/destroyed
				}
				if (isMatch(status, owner, start, end, nodes) == false) {
					continue;
				}

				/*
				 * NOTE: set is TreeSet, sorted by ReserveId.
				 * Overwrite if rsvId is duplicated.
				 */
				set.add(status);
			}

			return set;
		} catch (FileNotFoundException e) {
			String msg;
			if (file.exists()) {
				msg = String.format("Cannot open %s because of permission.",
						file.getAbsolutePath());
			} else {
				msg = String.format("%s not found. No reserves have be made.",
						file.getAbsolutePath());
			}
			throw new ReserveException(msg);
		} catch (IOException e) {
			throw new ReserveException("failed to read " + file.getAbsolutePath());
		} finally {
			if (reader != null) {
				try {
					reader.close();
				} catch (IOException e2) {
					throw new ReserveException("cannot close " + file.getAbsolutePath());
				}
			}
		}

	}

	public Calendar getLogStartedTime() {
		if (logStartedTime != null) {
			return logStartedTime;
		} else {
			assert(false);
			Calendar c = Calendar.getInstance();
			c.setTime(new Date(0L));
			return c;
		}
	}

}
